use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
use serde::{Deserialize, Serialize};
use crate::blockchain::{Block, Blockchain};
use crate::transaction::TXOutput;

pub struct UTXOSet {
    pub blockchain: Blockchain,
    pub data: HashMap<Vec<u8>, Vec<TXOutputIndexWrap>>,
}

#[derive(Serialize, Deserialize)]
#[derive(Clone)]
pub struct TXOutputIndexWrap {
    pub vout_idx: usize,
    pub out: TXOutput,
}

impl UTXOSet {
    pub fn reindedx(&mut self) {
        let utxos = self.blockchain.find_all_utxo();
        self.data = utxos;
    }

    pub fn update(&mut self, block: &Block) {
        for tx in block.transactions.iter() {
            if !tx.is_coninbase() {
                for vin in &tx.vin {
                    let mut update_outs = vec![];
                    let outs = self.data.get(&vin.txid).unwrap();

                    for out in outs {
                        if out.vout_idx != vin.vout {
                            update_outs.push(out.clone());
                        }
                    }

                    if update_outs.is_empty() {
                        self.persist_remove(&vin.txid);
                        self.data.remove(&vin.txid);
                    } else {
                        self.persist_insert(&vin.txid, &update_outs);
                        self.data.insert(vin.txid.clone(), update_outs);
                    }
                }
            }

            let mut add_outs = vec![];
            for vout_idx in 0..tx.vout.len() {
                add_outs.push(TXOutputIndexWrap {
                    vout_idx,
                    out: tx.vout[vout_idx].clone(),
                })
            }

            self.persist_insert(&tx.id, &add_outs);
            self.data.insert(tx.id.clone(), add_outs);
        }
    }

    pub fn find_utxo(&self, pub_key_hash: &Vec<u8>) -> Vec<TXOutput> {
        let mut utxos = vec![];
        for txid in self.data.keys() {
            let uouts = self.data.get(txid).unwrap();
            for out_wrap in uouts {
                if out_wrap.out.is_locked_with_key(pub_key_hash) {
                    utxos.push(out_wrap.out.clone());
                }
            }
        }

        utxos
    }

    pub fn find_spendable_outputs(&self, pub_key_hash: &Vec<u8>, amount: i32) -> (i32, HashMap<Vec<u8>, Vec<TXOutputIndexWrap>>) {
        let mut accumulated = 0;
        let mut unspent_outputs = HashMap::new();

        for txid in self.data.keys() {
            let outs = self.data.get(txid).unwrap();
            for out_wrap in outs {
                if out_wrap.out.is_locked_with_key(pub_key_hash) && accumulated < amount {
                    accumulated += out_wrap.out.value;
                    let unspent_outs_opt = unspent_outputs.get_mut(txid);
                    match unspent_outs_opt {
                        None => {
                            let out_list = vec![out_wrap.clone()];
                            unspent_outputs.insert(txid.clone(), out_list);
                        }
                        Some(list) => {
                            list.push(out_wrap.clone());
                        }
                    }
                }
            }
        }

        (accumulated, unspent_outputs)
    }

    pub(crate) fn load_from_file(&mut self) {
        let file = OpenOptions::new().read(true).append(true).open("./utxoset.txt").unwrap();
        let reader = BufReader::new(&file);
        let mut data = HashMap::new();

        // INSERT/REMOVE key value
        for line in reader.lines() {
            let line = line.unwrap();
            let mut spl = line.split(" ");
            let cmd = spl.next().unwrap();
            let key = spl.next().unwrap();
            if cmd == "INSERT" {
                let key = serde_json::from_str(key).unwrap();
                let value = spl.next().unwrap();
                let value = serde_json::from_str(value).unwrap();
                data.insert(key, value);
            } else if cmd == "REMOVE" {
                let key: Vec<u8> = serde_json::from_str(key).unwrap();
                data.remove(&key);
            } else {
                panic!();
            }
        }

        self.data = data;
    }

    pub fn persist(&self) {
        let mut file = OpenOptions::new().create(true).write(true).open("./utxoset.txt").unwrap();
        for key in self.data.keys() {
            let value = self.data.get(key).unwrap();
            file.write((String::from("INSERT ") + serde_json::to_string(key).unwrap().as_str() + " " +
                serde_json::to_string(value).unwrap().as_str() + "\n").as_bytes()).expect("panic");
        }
    }

    fn persist_insert(&self, key: &Vec<u8>, value: &Vec<TXOutputIndexWrap>) {
        let mut file = OpenOptions::new().create(true).write(true).append(true).open("./utxoset.txt").unwrap();
        file.write((String::from("INSERT ") + serde_json::to_string(key).unwrap().as_str() +
            " " + serde_json::to_string(value).unwrap().as_str() + "\n").as_bytes()).expect("panic");
    }

    fn persist_remove(&self, key: &Vec<u8>) {
        let mut file = OpenOptions::new().create(true).write(true).append(true).open("./utxoset.txt").unwrap();
        file.write((String::from("REMOVE ") + serde_json::to_string(key).unwrap().as_str() + "\n").as_bytes()).expect("panic");
    }
}